package core;
import java.util.Timer;
import java.util.TimerTask;

import javax.microedition.lcdui.Display;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

import colors.*;

/**
 * <p>World for programming interactive games on mobile devices in J2ME.
 * Currently supports graphics, standard key events, and a timer.</p>
 * 
 * <p>Designed to implement as much as possible, considering the design of J2ME,
 * the same interface as the iDraw library.</p>
 * 
 * @author Shayne Caswell
 *
 */
abstract public class WorldMidlet extends MIDlet {
		// World's Objects //
	/** The canvas that displays the current world */
	public WorldCanvas theCanvas;

	/** The timer for this world */
	protected Timer mytime;

		// End World's Objects //

		// World Status //
	
	/** Tracks whether or not the world has started and has not ended */
	protected boolean worldExists = false;

	/** Switch to determine if this app is dead (cannot be unpaused) */
	private boolean dead = false;

	/** Track whether or not this app is paused */
	protected boolean paused = false;

		// End World Status //

		// Begin World Setup //
	
	/** The color to be used as this MIDlet's background color */
	protected MobileColor background;

	/** The color to be used to draw on the background by default */
	protected MobileColor foreground;

	/** Title of this MIDlet (game) */
	public String title;

	/** the speed at which the timer will tick */
	protected int tickSpeed;

		// End World Setup //

	/**
	 * <p>Default constructor.</p>
	 * 
	 * @throws MIDletStateChangeException
	 */
	public WorldMidlet() throws MIDletStateChangeException{
	}

	/**
	 * <p>Should be overridden in the class that extends this one.
	 * This makes the app notify all threads that the app has started or resumed.</p>
	 * 
	 * <p>Such action is not necessary at the beginning of the app, but is needed
	 * when the MIDlet is resumed (unpaused).</p>
	 * 
	 * @throws MIDletStateChangeException
	 */
	protected void startApp() throws MIDletStateChangeException{
	}

	/**
	 * <p>Pauses the app. This is called, for instance, when the user receives
	 * a phone call, text message, etc. It may also be called in other ways.</p>
	 * 
	 * <p>Says the world does not exist. This causes the timer not to execute the tick event.
	 * It then notifies threads that it is paused.</p>
	 */
	protected void pauseApp() {
		this.worldExists = false;
		this.paused = true;
		this.notifyPaused();
	}

	/**
	 * <p>Signals the MIDlet to enter the Destroyed state.
	 * Although not yet implemented, it is possible to add functionality that
	 * in certain circumstances, the call to this function does not destroy the
	 * app. If the boolean parameter <code>unconditional</code> is false,
	 * throwing a <code>MIDletStateChangeException</code> will cancel this process.</p>
	 * 
	 * @param unconditional Determines whether this MIDlet may avoid entering
	 * the <i>Destroyed</i> state under certain conditions.
	 */
	protected void destroyApp(boolean unconditional) throws MIDletStateChangeException {
		this.mytime = null;
		this.theCanvas = null;
		this.dead = true;
		this.notifyDestroyed();
	}

	/**
	 * <p>The method that can be used to call big bang without specifying a foreground color.</p>
	 * 
	 * @param speed The tick speed of this <code>WorldMidlet</code>
	 * @param title The title of this game
	 * @param background The desired background color
	 * @throws MIDletStateChangeException
	 */
	public void bigBang(double speed, String title, MobileColor background) throws MIDletStateChangeException{
		this.bigBang(speed, title, background, background.opposite());
	}

	/**
	 * <p>The method that begins the game. This should be called in the 
	 * <code>startApp</code> method of the extending class. This is called
	 * when the MIDlet is initialized, and again every time the MIDlet returns
	 * from the <i>paused</i> state.</p>
	 * 
	 * @param speed The tick speed of this <code>WorldMidlet</code>
	 * @param title The title of this game
	 * @param background The desired background <code>MobileColor</code>
	 * @param foreground The desired foreground <code>MobileColor</code>
	 * @throws MIDletStateChangeException
	 */
	public void bigBang(double speed, String title, MobileColor background, MobileColor foreground) throws MIDletStateChangeException{
		this.tickSpeed = (new Double(speed * 1000)).intValue();
		this.title = title;
		this.background = background;
		this.foreground = foreground;

		this.worldExists = true;
		if(paused){
			this.resumeRequest();
		}
		this.paused = false;

		// if this game is dead for whatever reason, do not continue creating game.
		if(this.dead){
			return;
		}

		// if the canvas hasn't been instantiated, create it and set it up
		if(theCanvas == null){
			this.theCanvas = new WorldCanvas(this);
			theCanvas.setTitle(this.title);
			Display.getDisplay(this).setCurrent(this.theCanvas);
			this.theCanvas.clear(this.background);
		}

		// if tickspeed is invalid, kill this game
		if(tickSpeed <= 0){
			this.endOfTime("Invalid tick speed!", this.foreground);
			return;
		}

		// if the timer does not yet exist, instantiate it
		if(mytime == null){
			mytime = new Timer();

			// schedule processTick() with this timer
			mytime.scheduleAtFixedRate(new drawTask(this), 0, this.tickSpeed);
		}
	}

	protected void processTick() {
		try{
			if (worldExists){
				this.onTick();
				this.drawWorld();
			}
		} 
		catch(RuntimeException re){
			re.printStackTrace();
			endOfTime(re.getMessage(), this.foreground);
		}
	}

	/**
	 * <p>The method invoked by the {@link WorldCanvas WorldCanvas} on selected key events.
	 * Delegates to the user to define a new state of the world,
	 * then resets the canvas and event handlers for the new world
	 * to those currently used.</p>
	 * 
	 * @param key The <code>Button</code> encapsulating data about the key that was pressed
	 */
	protected void processKeyEvent(Button key){
		try{
			if (worldExists){
				this.onKeyEvent(key);
				this.drawWorld();
			}
		}
		catch(RuntimeException re){
			re.printStackTrace();
			this.endOfTime(re.getMessage(), this.foreground);
		}
	}

	/**
	 * <p>The method invoked by the {@link WorldCanvas WorldCanvas} when a pointer is pressed on
	 * the device. Delegates to the user to define a new world state based upon the given <code>Posn</code>.</p>
	 * 
	 * @param p The <code>Posn</code> that represents the point where a pointer was pressed.
	 */
	protected void processPointerEvent(Posn p){
		this.onPointerEvent(p);
	}

	/**
	 * <p>The method invoked by the {@link WorldCanvas WorldCanvas} when a pointer is dragged on
	 * the device. Delegates to the user to define a new world state based upon the given <code>Posn</code>.</p>
	 * 
	 * @param p The <code>Posn</code> that represents the point where a pointer was dragged to.
	 */
	protected void processDragEvent(Posn p) {
		this.onDragEvent(p);
	}

	/**
	 * <p>Abstract method the user must define to be called upon each tick of
	 * the <code>Timer</code>.</p>
	 */
	abstract public void onTick();
	
	/**
	 * <p>Abstract method the user must define to be called when the world
	 * needs to be drawn or redrawn.</p>
	 */
	abstract public void draw();
	
	/**
	 * <p>Abstract method the user must define to be called each time a key is pressed.</p>
	 */
	abstract public void onKeyEvent(Button s);

	/**
	 * <p>Abstract method the user must define to be called upon each time a pointer
	 * is pressed on the screen.</p>
	 */
	abstract public void onPointerEvent(Posn p);

	/**
	 * <p>Abstract method the user must define to be called upon each time a pointer
	 * is dragged on the screen.</p>
	 */
	abstract public void onDragEvent(Posn p);
	
	/**
	 * <p>If the world exists, clear the screen. Call the user's <code>draw</code>
	 * function to draw the new world to the <code>WorldCanvas</code>'s image buffer.
	 * Finally, call repaint to paint the buffer to the screen.</p>
	 */
	protected void drawWorld(){
		if (this.worldExists){
			this.theCanvas.clear(this.background);
			this.draw();
			this.theCanvas.repaint();
		}
	}

	/**
	 * Stop the timer, disable event processing - leave the canvas open, 
	 * write the end of time message onto the canvas
	 * 
	 * @param s The string to display on screen
	 * @param The <code>MobileColor</code> to paint the string in
	 */
	public void endOfTime(String s){
		// remove listeners and set worldExists to false
		if(mytime != null){
			mytime.cancel();
		}
		this.worldExists = false;
		System.out.println("The world stopped: " + s);

		// draw the final scene of the world with the end of time message
		this.theCanvas.clear(this.background);
		if(!this.dead){
			this.draw();
		}
		this.theCanvas.drawString(new Posn(10, theCanvas.getHeight() * 9 / 10),s, new Black());
		this.theCanvas.repaint();
		this.dead = true;
	}
	
	/**
	 * Stop the timer, disable event processing - leave the canvas open, 
	 * write the end of time message onto the canvas
	 * 
	 * @param s The string to display on screen
	 * @param The <code>MobileColor</code> to paint the string in
	 */
	public void endOfTime(String s, MobileColor color){
		// remove listeners and set worldExists to false
		if(mytime != null){
			mytime.cancel();
		}
		this.worldExists = false;
		System.out.println("The world stopped: " + s);

		// draw the final scene of the world with the end of time message
		this.theCanvas.clear(this.background);
		if(!this.dead){
			this.draw();
		}
		this.theCanvas.drawString(new Posn(10, theCanvas.getHeight() * 9 / 10),s, color);
		this.theCanvas.repaint();
		this.dead = true;
	}

	/** 
	 * End the interactions of this world, and graphically display
	 * an end of world message to notify the user that the MIDlet has ended.
	 * 
	 * @param s The string to display on the canvas at the end of the world
	 * @param color The color to write the given string in
	 */
	public void endOfWorld(String s, MobileColor color){
		// remove listeners and set worldExists to false
		if(mytime != null){
			mytime.cancel();
		}
		this.worldExists = false;
		System.out.println("The end of the world: " + s);

		// draw the final scene of the world with the end of world message
		this.theCanvas.clear(this.background);
		if(!this.dead){
			this.draw();
		}
		this.theCanvas.drawString(new Posn(10, theCanvas.getHeight() * 9 / 10),s, color);
		this.theCanvas.repaint();
		this.dead = true;
	}

	/**
	 * Private class to extend TimerTask.
	 * Used solely to register the WorldMidlet's processTick() function
	 * to its Timer.
	 * 
	 * @author Shayne Caswell
	 * 
	 */
	private class drawTask extends TimerTask{
		/** Reference to the WorldMidlet */
		private WorldMidlet theWorld;

		/** 
		 * Constructor takes the WorldMidlet that is running.
		 * 
		 * @param theWorld The WorldMidlet that is running.
		 */
		public drawTask(WorldMidlet theWorld){
			this.theWorld = theWorld;
		}

		/** 
		 * Run the WorldMidlet's processTick function at every tick of the Timer.
		 */
		public void run() {
			if(theWorld.worldExists){
				theWorld.processTick();
			}
		}
	}
}